﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Reflection;
using System.Diagnostics;
using System.Collections.Concurrent;
using System.Threading;
using System.Text;
using System.Diagnostics.Contracts;

using King.Text;
using King.Extensions;
using King.Collections;
using King.Declarative;
using System.Collections;
using System.ComponentModel;

namespace King.Automation {

    internal static class ProcessStreamEncoding {

        static ProcessStreamEncoding() {
            Utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true, throwOnInvalidBytes: true);
            Unicode = new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true);
        }

        private static readonly Encoding Unicode;
        private static readonly Encoding Utf8;

        internal static string ToString(this byte[] buffer, int count) {
            Contract.Requires(buffer != null);

            string result = null;

            // use a hursitic (no null bytes) to test if this is in console encoding
            if (buffer.Take(count).None(o => o == 0)) 
                result = Utf8.GetString(buffer, 0, count);
             else
                result = Unicode.GetString(buffer, 0, count);

            return result;
        }
    }

    public class AutomationProcess : TextDeclaration {

        private const int BufferSize = 4096;

        private object m_exe;
        private object m_currentDirectory;
        private object m_redirectTarget;
        private Func<string, bool> m_onError;
        private int[] m_warnings;

        public AutomationProcess(object exe, 
            object currentDirectory = null, 
            object redirectTarget = null,
            Func<string, bool> onError = null,
            int[] warnings = null) {

            m_exe = exe;
            m_currentDirectory = currentDirectory ?? Environment.CurrentDirectory.NormalizeDirectory();
            m_redirectTarget = redirectTarget;
            m_onError = onError;
            m_warnings = warnings;
        }

        private string FindOnPath(string exe) {
            if (File.Exists(exe))
                return exe;

            if (new Uri(exe, UriKind.RelativeOrAbsolute).IsAbsoluteUri)
                throw new ArgumentException("'" + exe + "' not found.");

            var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location).NormalizeDirectory();

            var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
            var dirs = pathEnv.StringSplit(";").Concat(assemblyDir);
            var paths =
               (from o in dirs
                let url = new Uri(o.Trim().Trim('%'), UriKind.RelativeOrAbsolute)
                where url.IsAbsoluteUri
                select new Uri(url.NormalizeUrlDirectory(), exe).LocalPath)
                .ToArray();

            var path = paths.FirstOrDefault(o => File.Exists(o));

            if (path == null)
                throw new ArgumentException(new TextHeader("'" + exe + "' not found on path. Searched: ") { paths }.ReadToEnd());

            return exe;
        }
        private Process CreateProcess(string exe, string arguments, string currentDirectory) {
            var process = new Process();

            process.StartInfo = new ProcessStartInfo(exe, arguments) {
                WorkingDirectory = currentDirectory ?? Environment.CurrentDirectory,
                WindowStyle = ProcessWindowStyle.Hidden,
                CreateNoWindow = true,

                RedirectStandardOutput = true,
                RedirectStandardError = true,

                UseShellExecute = false,
                ErrorDialog = false,
            };

            return process;
        }
        private IEnumerable Execute(string exe, string arguments, string currentDirectory, string redirectTarget) {

            // create and start the process
            var process = CreateProcess(exe, arguments, currentDirectory);
            process.Start();

            var outputStream = process.StandardOutput.BaseStream;
            var errorStream = process.StandardError.BaseStream;

            // create buffers for asynchronous reads
            var outputBuffer = new byte[BufferSize];
            var errorBuffer = new byte[BufferSize];

            // buffer error messages for batch delivery to OnError
            var errorMessage = new StringBuilder();

            var outputResult = outputStream.BeginRead(outputBuffer, 0, BufferSize, null, null);
            var errorResult = errorStream.BeginRead(errorBuffer, 0, BufferSize, null, null);

            var outputWaitHandle = outputResult.AsyncWaitHandle;
            var errorWaitHandle = errorResult.AsyncWaitHandle;

            bool outputLastRead = false;
            bool errorLastRead = false;

            while(true) {

                // gather non-null waithandles
                var waitHandles = new[] { 
                    outputWaitHandle, 
                    errorWaitHandle, 
                }.WhereNotDefault().ToArray();

                if (waitHandles.None())
                    break;

                // wait for the streams to produce data
                var waitHandle = waitHandles[WaitHandle.WaitAny(waitHandles)];

                // output stream produced data
                if (waitHandle == outputWaitHandle) {
                    outputWaitHandle = null;

                    var count = outputStream.EndRead(outputResult);

                    // if asked to redirect then appened the buffer to a file
                    if (redirectTarget != null) {
                        using (var fileStream = new FileStream(redirectTarget, FileMode.Append))
                            new MemoryStream(outputBuffer, 0, count).CopyTo(fileStream);

                    // else return the result directly
                    } else {
                        var output = outputBuffer.ToString(count);
                        yield return output;
                    }

                    if (!outputLastRead) {
                        outputLastRead = process.HasExited;
                        outputResult = outputStream.BeginRead(outputBuffer, 0, BufferSize, null, null);
                        outputWaitHandle = outputResult.AsyncWaitHandle;
                    }
                }

                // error stream produced data
                if (waitHandle == errorWaitHandle) {
                    errorWaitHandle = null;

                    var count = errorStream.EndRead(errorResult);
                    var error = errorBuffer.ToString(count);
                    errorMessage.Append(error);

                    if (!errorLastRead) {
                        errorLastRead = process.HasExited;
                        errorResult = errorStream.BeginRead(errorBuffer, 0, BufferSize, null, null);
                        errorWaitHandle = errorResult.AsyncWaitHandle;
                    }
                }
            }

            // if anything was written to the error stream raise OnError to see if an exception should be throwns
            if (errorMessage.Length > 0) {
                if (m_onError == null || !m_onError(errorMessage.ToString()))
                    throw new ArgumentException(new TextSingleSpaced {
                        "Calling an external process resulted in output to the error stream.",
                        new TextCommandLine(currentDirectory) { exe, arguments },
                        errorMessage
                    }.ReadToEnd());
            }

            var exitCode = process.ExitCode;
            if (exitCode != 0) {
                var exception = new Win32Exception(process.ExitCode);
                if (m_warnings == null || !m_warnings.Contains(exitCode))
                    throw exception;
                yield return exception.Message;
            }
        }

        protected override IEnumerable Children(DeclarationContext frame) {
            var exe = FindOnPath(frame.ToString(m_exe));
            var arguments = frame.ToString(Children());
            var currentDirectory = frame.ToString(m_currentDirectory);

            var redirectTarget = frame.ToString(m_redirectTarget);
            string tempRedirectTarget = null;
            if (redirectTarget != null)
                tempRedirectTarget = Path.GetTempFileName();

            try {

                // execute!
                yield return Execute(exe, arguments, currentDirectory, tempRedirectTarget);

                // move temp redirect target to redirect target
                if (redirectTarget != null) {
                    var dir = Path.GetDirectoryName(redirectTarget);
                    if (!Directory.Exists(dir))
                        Directory.CreateDirectory(dir);
                    File.Move(tempRedirectTarget, redirectTarget);
                }

            } finally {

                // cleanup temp redirect target
                if (File.Exists(tempRedirectTarget))
                    File.Delete(tempRedirectTarget);
            }
        }
    }
}